package org.siteanalyser.service.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.siteanalyser.domain.AnalysisReport;
import org.siteanalyser.domain.Message;
import org.siteanalyser.service.Service;
import org.siteanalyser.service.SystemException;
import org.siteanalyser.util.XmlUtils;
import org.springframework.beans.factory.annotation.Required;

import com.seomoz.api.authentication.Base64;

public class AwsServiceImpl implements Service {
    private static final Log log = LogFactory.getLog(AwsServiceImpl.class);
    private static final String ACTION_NAME = "UrlInfo";
    private static final String RESPONSE_GROUP_NAME = "RelatedLinks,Categories,Rank,UsageStats,Speed,Keywords,LinksInCount";
    private static final String AWS_BASE_URL = "http://awis.amazonaws.com?";
    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    private static final String DATEFORMAT_AWS = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
    private String accessKey;
    private String secretKey;

    @Required
    public void setAccessKey(final String accessKey) {
        this.accessKey = accessKey;
    }

    @Required
    public void setSecretKey(final String secretKey) {
        this.secretKey = secretKey;
    }

    public void process(final Message message) throws SystemException {
        try {
            awsReport(message.getRequest().getUrl(), message.getReport());
        } catch (final SignatureException e) {
            throw new SystemException(e);
        } catch (final MalformedURLException e) {
            throw new SystemException(e);
        } catch (final IOException e) {
            throw new SystemException(e);
        }
    }

    private void awsReport(final String url, final AnalysisReport result) throws SignatureException,
            MalformedURLException, IOException {
        final String timestamp = getTimestampFromLocalTime(Calendar.getInstance().getTime());
        final String signature = generateSignature(ACTION_NAME + timestamp, secretKey);
        final StringBuffer urlBuffer = new StringBuffer(AWS_BASE_URL);
        urlBuffer.append("&Action=");
        urlBuffer.append(ACTION_NAME);
        urlBuffer.append("&ResponseGroup=");
        urlBuffer.append(RESPONSE_GROUP_NAME);
        urlBuffer.append("&AWSAccessKeyId=");
        urlBuffer.append(accessKey);
        urlBuffer.append("&Signature=");
        urlBuffer.append(URLEncoder.encode(signature, "UTF-8"));
        urlBuffer.append("&Timestamp=");
        urlBuffer.append(URLEncoder.encode(timestamp, "UTF-8"));
        urlBuffer.append("&Url=");
        urlBuffer.append(URLEncoder.encode(url, "UTF-8"));
        log.debug("About to make AWS request: " + urlBuffer.toString());
        final String xmlResponse = makeAwsRequest(urlBuffer.toString());
        log.debug("Response from AWS: " + xmlResponse);
        result.addReportItem("alexabacklinks", XmlUtils.getXpathElementValue(xmlResponse, "//aws:LinksInCount/text()"));
    }

    /**
     * Generate a timestamp for use with AWS request signing
     * 
     * @param date
     *            current date
     * @return timestamp
     */
    private static String getTimestampFromLocalTime(final Date date) {
        final SimpleDateFormat format = new SimpleDateFormat(DATEFORMAT_AWS);
        format.setTimeZone(TimeZone.getTimeZone("GMT"));
        return format.format(date);
    }

    /**
     * Computes RFC 2104-compliant HMAC signature.
     * 
     * @param data
     *            The data to be signed.
     * @param key
     *            The signing key.
     * @return The base64-encoded RFC 2104-compliant HMAC signature.
     * @throws java.security.SignatureException
     *             when signature generation fails
     */
    private static String generateSignature(final String data, final String key)
            throws java.security.SignatureException {
        String result;
        try {
            final SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
            final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
            mac.init(signingKey);
            final byte[] rawHmac = mac.doFinal(data.getBytes());
            result = Base64.encodeBytes(rawHmac);
        } catch (final NoSuchAlgorithmException e) {
            throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
        } catch (final InvalidKeyException e) {
            throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
        }
        return result;
    }

    /**
     * Make a request to the specified Url and return the results as a String
     * 
     * @param urlBuffer
     * @return the XML document as a String
     * @throws java.net.MalformedURLException
     * @throws IOException
     */
    private static String makeAwsRequest(final String requestUrl) throws java.net.MalformedURLException, IOException {
        final URL url = new URL(requestUrl);
        final URLConnection conn = url.openConnection();
        final InputStream in = conn.getInputStream();

        final StringBuffer sb = new StringBuffer();
        int c;
        int lastChar = 0;
        while ((c = in.read()) != -1) {
            if (c == '<' && (lastChar == '>'))
                sb.append('\n');
            sb.append((char) c);
            lastChar = c;
        }
        in.close();
        return sb.toString();
    }

}
